﻿using Microsoft.Office.InfoPath;
using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;

namespace InfoPathHelpers.RepeatingTable
{
    public static class RepeatingTableHelpers
    {
        /// <summary>
        /// Clears all rows from a repeating table where all of the specified fields are blank.
        /// </summary>
        /// <param name="context">The datasource in which to look for the table.</param>
        /// <param name="tableXPath">The XPath to the repeating table.</param>
        /// <param name="options">Options used to decide whether a row should be deleted or not.</param>
        /// <param name="tableFields">A list of fields (including namespace) to check for blanks.</param>
        public static void Clear(IDataSourceContext context, string tableXPath, RowSelectionOption options, params string[] tableFields)
        {
            int RowCount = context.Select(tableXPath).Count;
            bool leaveFirstRow = (options & RowSelectionOption.LeaveFirstRow) == RowSelectionOption.LeaveFirstRow;
            bool IsGroup = context.Select(tableXPath + "/*").Count > 0;
            if (RowCount < 1 || (RowCount == 1 && leaveFirstRow))
            {
                // Return if there are no rows, or there is only one row and it is not to be deleted.
                return;
            }

            bool ZeroCountsAsEmpty = (options & RowSelectionOption.ZeroCountsAsEmpty) == RowSelectionOption.ZeroCountsAsEmpty;
            XPathNodeIterator SelectedNodes = null;


            if (((options & RowSelectionOption.All) == RowSelectionOption.All) ||
                ((options ^ RowSelectionOption.LeaveFirstRow) == 0))
            {
                // Clear all rows.
                XPathNavigator FirstNode = context.SelectSingleNode(tableXPath);
                XPathNavigator LastNode = context.SelectSingleNode(tableXPath + "[last()]");
                if (FirstNode == null)
                {
                    throw new ArgumentException("XPath expression must evaluate to a node", "xPath");
                }
                if (leaveFirstRow && FirstNode.ComparePosition(LastNode) == XmlNodeOrder.Before)
                {
                    FirstNode.MoveToNext(XPathNodeType.Element);
                }
                FirstNode.DeleteRange(LastNode);
            }
            else if (!IsGroup)
            {
                // For repeating elements there is only one field so 
                // specifying any option is equivalent (it is either blank or not).
                tableXPath += "[string-length(normalize-space(.)) = 0";
                if (ZeroCountsAsEmpty)
                {
                    tableXPath += " or . = 0";
                }
                tableXPath += "]";
                SelectedNodes = context.Select(tableXPath);
            }
            else if ((options & RowSelectionOption.Blanks) == RowSelectionOption.Blanks)
            {
                // Clear complete blanks.
                // XPath format is Root/Group[count(*[string-length(.) > 0 and . != 0]) = 0].
                // This returns Group elements where all child elements are blank,
                // or in the case where zero is considered blank all child elements are blank or zero.
                tableXPath += "[count(*[string-length(normalize-space(.)) > 0";
                if (ZeroCountsAsEmpty)
                {
                    tableXPath += " and . != 0";
                }
                tableXPath += "]) = 0]";

                SelectedNodes = context.Select(tableXPath);
            }
            else if ((options & RowSelectionOption.AnyFieldBlank) == RowSelectionOption.AnyFieldBlank)
            {
                // Clear where any field is blank.
                if (tableFields.Length == 0)
                {
                    // If no fields are specified then we simply revert to selecting
                    // group elements where any field is blank.
                    tableXPath += "[*[string-length(normalize-space(.)) = 0";
                    if (ZeroCountsAsEmpty)
                    {
                        tableXPath += " or . = 0";
                    }
                    tableXPath += "]]";

                    SelectedNodes = context.Select(tableXPath);
                }
                else
                {
                    // If fields are specified we create an XPath expression which
                    // selects any group elements where children who's local name matches
                    // the specified fields have no value.
                    tableXPath += "[*[(";
                    foreach (string Field in tableFields)
                    {
                        string LocalName = Field;
                        int ColonIndex = Field.IndexOf(':');
                        if (ColonIndex != -1)
                        {
                            LocalName = Field.Remove(0, ColonIndex + 1);
                        }
                        tableXPath += "local-name() = '" + LocalName + "' or ";
                    }
                    tableXPath = tableXPath.Remove(tableXPath.Length - 3);
                    tableXPath += ") and (string-length(normalize-space(.)) = 0";
                    if (ZeroCountsAsEmpty)
                    {
                        tableXPath += " or . = 0";
                    }
                    tableXPath += ")]]";
                    SelectedNodes = context.Select(tableXPath);
                }
            }
            else if ((options & RowSelectionOption.AllFieldsBlank) == RowSelectionOption.AllFieldsBlank)
            {
                // Clear where all fields are blank.
                // If no fields are specified we revert to selecting group 
                // elements where ALL fields are blank.
                if (tableFields.Length == 0)
                {
                    tableXPath += "[count(./*[string-length(normalize-space(.)) > 0";
                    if (ZeroCountsAsEmpty)
                    {
                        tableXPath += " and . != 0";
                    }
                    tableXPath += "]) = 0]";

                    SelectedNodes = context.Select(tableXPath);
                }
                else
                {
                    // If fields are specified we create an XPath expression to select
                    // group elements where the number of child elements with names matching 
                    // the supplied fields which have a value is 0.
                    tableXPath += "[count(*[(";
                    foreach (string Field in tableFields)
                    {
                        string LocalName = Field;
                        int ColonIndex = Field.IndexOf(':');
                        if (ColonIndex != -1)
                        {
                            LocalName = Field.Remove(0, ColonIndex + 1);
                        }
                        tableXPath += "local-name() = '" + LocalName + "' or ";
                    }
                    tableXPath = tableXPath.Remove(tableXPath.Length - 3);
                    tableXPath += ") and (string-length(normalize-space(.)) > 0";
                    if (ZeroCountsAsEmpty)
                    {
                        tableXPath += " or . != 0";
                    }
                    tableXPath += ")]) = 0]";
                    SelectedNodes = context.Select(tableXPath);
                }
            }

            List<XPathNavigator> NodesToDelete = new List<XPathNavigator>();

            if (SelectedNodes != null)
            {
                // Copy the nodes to a list.
                foreach (XPathNavigator Node in SelectedNodes)
                {
                    NodesToDelete.Add(Node);
                }
            }

            if (RowCount == NodesToDelete.Count && leaveFirstRow)
            {
                // If all nodes are to be deleted but the first row should be left then remove it from the list.
                NodesToDelete.RemoveAt(0);
            }

            // Perform delete operations.
            foreach (XPathNavigator Node in NodesToDelete)
            {
                Node.DeleteSelf();
            }
        }

        /// <summary>
        /// Clears all rows from a repeating table using an XPath expression to match rows.
        /// </summary>
        /// <param name="context">The datasource in which to look for the table.</param>
        /// <param name="tableXPath">The XPath to the repeating table.</param>
        /// <param name="matchRelativeXPath">The relative XPath to the field to be used to match rows, 
        /// or null to use the row itself.</param>
        /// <param name="matchCondition">The value of the match field which should be selected. (Case Sensitive)</param>
        public static void Clear(IDataSourceContext context, string tableXPath, string matchRelativeXPath, object matchCondition)
        {
            string XPathExpression = string.Empty;

            if (string.IsNullOrEmpty(matchRelativeXPath))
            {
                XPathExpression = string.Format("{0}[. = '{1}']", tableXPath, matchCondition);
            }
            else
            {
                XPathExpression = string.Format("{0}[{1} = '{2}']", tableXPath, matchRelativeXPath, matchCondition.ToString().ToLower());
            }

            var MatchingNodes = context.Select(XPathExpression);

            List<XPathNavigator> NodesToDelete = new List<XPathNavigator>();
            
            foreach (XPathNavigator Node in MatchingNodes)
            {
                NodesToDelete.Add(Node);
            }

            foreach (XPathNavigator Node in NodesToDelete)
            {
                Node.DeleteSelf();
            }
        }

        /// <summary>
        /// Used on a repeating table with XmlChangingEvent to prevent deletion of the last row from the table.
        /// </summary>
        /// <param name="sender">The sender of the changing event.</param>
        /// <param name="e">The XmlChangingEventArgs of the changing event.</param>
        /// <returns>True if the operation was cancelled otherwise false.</returns>
        public static bool PreventFirstRowDeletion(object sender, XmlChangingEventArgs e)
        {
            if (e.Operation == XmlOperation.Delete)
                if (e.OldParent.SelectChildren(XPathNodeType.Element).Count < 1)
                {
                    e.CancelableArgs.Cancel = true;
                    e.CancelableArgs.Message = "This row cannot be deleted";
                    e.CancelableArgs.MessageDetails = "The first row in this repeating table is not allowed to be deleted";
                    return true;
                }
            return false;
        }
    }

    /// <summary>
    /// Sets options for deciding whether a row should be deleted or not.
    /// Note that except for LeaveFirstRow all options follow a precedence. i.e Selecting "All | Blanks" 
    /// will result in all rows being cleared.
    /// </summary>
    [Flags]
    public enum RowSelectionOption
    {
        None = 0,
        /// <summary>
        /// Clears all rows from the repeating table.
        /// </summary>
        All = 1 << 0,
        /// <summary>
        /// Clears only completely blank rows from the table.
        /// </summary>
        Blanks = 1 << 1,
        /// <summary>
        /// Clears rows where any of the specified fields are blank. If no fields are specified all fields will be checked.
        /// </summary>
        AnyFieldBlank = 1 << 2,
        /// <summary>
        /// Clears rows where all specified fields are blank. If no fields are specified all fields will be checked.
        /// </summary>
        AllFieldsBlank = 1 << 3,
        /// <summary>
        /// Leaves at least one row in the table.
        /// </summary>
        LeaveFirstRow = 1 << 4,
        /// <summary>
        /// Fields which have 0 as a value are considered empty.
        /// </summary>
        ZeroCountsAsEmpty = 1 << 5
    }
}
